Skip to content

fix(core): field unmount#2068

Open
TkDodo wants to merge 13 commits intoTanStack:mainfrom
TkDodo:feature/fix/field-unmount
Open

fix(core): field unmount#2068
TkDodo wants to merge 13 commits intoTanStack:mainfrom
TkDodo:feature/fix/field-unmount

Conversation

@TkDodo
Copy link

@TkDodo TkDodo commented Mar 6, 2026

Add unmounting to fields. field.mount now returns a cleanup function for unmounting.

Without unmounting, conditionally rendered fields will keep their field-level validations running even though the field isn’t rendered anymore.

Summary by CodeRabbit

  • New Features

    • Added form option cleanupFieldsOnUnmount (default: false) to control clearing field meta/state on unmount.
    • Field mount now returns an explicit cleanup/unmount function.
  • Bug Fixes

    • Unmount now cancels in-flight async validations and debounced listeners to avoid stale callbacks.
    • Hidden/conditionally rendered fields no longer retain submit errors after unmount when cleanup is enabled.
  • Tests

    • Expanded coverage for mount/unmount, value/meta preservation, async cancellation, and remount behavior.

@changeset-bot
Copy link

changeset-bot bot commented Mar 6, 2026

🦋 Changeset detected

Latest commit: b68b8c0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@tanstack/form-core Patch
@tanstack/react-form Patch
@tanstack/angular-form Patch
@tanstack/form-devtools Patch
@tanstack/lit-form Patch
@tanstack/solid-form Patch
@tanstack/svelte-form Patch
@tanstack/vue-form Patch
@tanstack/react-form-nextjs Patch
@tanstack/react-form-remix Patch
@tanstack/react-form-start Patch
@tanstack/react-form-devtools Patch
@tanstack/solid-form-devtools Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Fixes the issue with field unmount in core.
@nx-cloud
Copy link

nx-cloud bot commented Mar 6, 2026

View your CI Pipeline Execution ↗ for commit b68b8c0

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 48s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-14 08:49:54 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 6, 2026

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@2068

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@2068

@tanstack/form-devtools

npm i https://pkg.pr.new/@tanstack/form-devtools@2068

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@2068

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@2068

@tanstack/react-form-devtools

npm i https://pkg.pr.new/@tanstack/react-form-devtools@2068

@tanstack/react-form-nextjs

npm i https://pkg.pr.new/@tanstack/react-form-nextjs@2068

@tanstack/react-form-remix

npm i https://pkg.pr.new/@tanstack/react-form-remix@2068

@tanstack/react-form-start

npm i https://pkg.pr.new/@tanstack/react-form-start@2068

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@2068

@tanstack/solid-form-devtools

npm i https://pkg.pr.new/@tanstack/solid-form-devtools@2068

@tanstack/svelte-form

npm i https://pkg.pr.new/@tanstack/svelte-form@2068

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@2068

commit: 49c1f7b

@TkDodo TkDodo marked this pull request as ready for review March 6, 2026 12:48
@sentry
Copy link

sentry bot commented Mar 6, 2026

Codecov Report

❌ Patch coverage is 92.30769% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.24%. Comparing base (6892ed0) to head (b68b8c0).
⚠️ Report is 154 commits behind head on main.

Files with missing lines Patch % Lines
packages/form-core/src/FieldApi.ts 92.10% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2068      +/-   ##
==========================================
- Coverage   90.35%   90.24%   -0.11%     
==========================================
  Files          38       49      +11     
  Lines        1752     2041     +289     
  Branches      444      533      +89     
==========================================
+ Hits         1583     1842     +259     
- Misses        149      179      +30     
  Partials       20       20              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@TkDodo TkDodo requested a review from crutchcorn March 7, 2026 08:36
@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Form option cleanupFieldsOnUnmount; FieldApi.mount now returns an explicit teardown function that may abort validations, cancel timers/listeners, reset per-field meta, and clear the field instance; tests and a changeset were added.

Changes

Cohort / File(s) Summary
Changeset
​.changeset/red-hats-jam.md
New changeset recording patch bumps for @tanstack/form-core and @tanstack/react-form and noting the core field unmount fix.
Form options & types
packages/form-core/src/FormApi.ts
Added public cleanupFieldsOnUnmount?: boolean (default false). Changed fieldInfo to a Partial<Record<...>> and removed two ESLint suppression comments.
Field lifecycle / mount API
packages/form-core/src/FieldApi.ts
FieldApi.mount now documents and returns an unmount/teardown function. Teardown (when enabled) aborts in-flight validations, clears timeouts/listeners, resets per-field meta (preserving interaction flags), nulls fieldInfo.instance, and guards against tearing down newer instances. FieldInfo lookups standardized.
Core tests
packages/form-core/tests/FieldApi.spec.ts
Added extensive tests for unmount behavior: meta preservation vs. clearing, async validation cancellation/ignore, debounced listener cancellation, newer-instance protection, value persistence across remounts, array/nested scenarios, and various validator triggers.
React integration test
packages/react-form/tests/useField.test.tsx
Added test verifying hidden field submit errors are cleared on unmount when cleanupFieldsOnUnmount: true, while values persist and submit validators run correctly after remount.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Form as FormApi
    participant Field as FieldApi
    participant Val as ValidationSystem

    App->>Form: create(formOptions with cleanupFieldsOnUnmount=true)
    App->>Field: mount field
    Field->>Form: register field instance & metadata
    Field->>Val: start validations / set timeouts / listeners
    Field-->>App: return cleanup function

    Note over Val: validations/timeouts/listeners in-flight

    App->>Field: call cleanup() on unmount
    Field->>Val: abort controllers & cancel timeouts/listeners
    Val-->>Val: stop async callbacks / ignore late results
    Field->>Form: reset field meta (or preserve depending on option), clear instance reference
    Field-->>App: cleanup complete
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibbled code where fields might dwell,
I cancel flights and clear each trail,
I keep the value, tidy the map,
Protect the new while old ones nap,
Hoppity hop — the form is hale.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description lacks the required template structure including the '🎯 Changes' section details, '✅ Checklist', and '🚀 Release Impact' sections, though it does explain the core concept. Follow the repository's PR description template by adding the required sections: '🎯 Changes' with detailed change description, '✅ Checklist' with contribution steps confirmation, and '🚀 Release Impact' with changeset acknowledgment.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(core): field unmount' clearly and concisely describes the main change: adding unmount cleanup functionality to fields in the core module.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can generate a title for your PR based on the changes with custom instructions.

Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/form-core/src/FieldApi.ts`:
- Around line 1361-1375: The abort loop currently runs before checking whether
this FieldApi instance is still the active one, which can cancel validations for
a newer remounted instance; move the instance check so it runs before touching
shared state (i.e., check fieldInfo.instance !== this and return early), or
alternatively inside the loop verify fieldInfo.instance === this before calling
validationMeta?.lastAbortController.abort() and clearing entries; update the
code paths around fieldInfo, validationMetaMap, lastAbortController, and the
instance guard to ensure only the active FieldApi instance aborts and clears the
shared validationMetaMap.
- Around line 1377-1383: The current reset in FieldApi using
this.form.baseStore.setState(...) replaces prev.fieldMetaBase[this.name] with
defaultFieldMeta and thus wipes preserved state on unmount/remount; update the
setter to merge with any existing meta for this.name instead of overwriting so
preserved flags/values survive (e.g., read existing =
prev.fieldMetaBase?.[this.name], then set fieldMetaBase[this.name] = {
...defaultFieldMeta, ...existing } or selectively preserve keys like
touched/value/defaultValueSeeded) so the logic in FieldApi that reseeds
options.defaultValue (referenced by the FieldApi methods around lines that
handle defaultValue reseeding) no longer loses user-entered values on remount.

In `@packages/react-form/tests/useField.test.tsx`:
- Around line 422-423: The test asserts onSubmit immediately after clicking
submit but form.handleSubmit() executes asynchronously; update the test to wait
for the async submit path to complete before asserting by awaiting a wait helper
that checks onSubmit (e.g., use waitFor or another async wait) so the assertion
verifies that onSubmit was called after form._handleSubmit()/form.handleSubmit()
finishes; reference the submitButton click and the onSubmit mock in the updated
assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a2c69fac-1e7b-4d00-b470-721c4ba1c718

📥 Commits

Reviewing files that changed from the base of the PR and between a4e8dec and 4eff6fa.

📒 Files selected for processing (5)
  • .changeset/red-hats-jam.md
  • packages/form-core/src/FieldApi.ts
  • packages/form-core/src/FormApi.ts
  • packages/form-core/tests/FieldApi.spec.ts
  • packages/react-form/tests/useField.test.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/form-core/src/FieldApi.ts`:
- Around line 1364-1375: The teardown skips aborting async work started by the
older FieldApi instance because it only avoids touching shared controllers when
a newer instance is mounted; fix by either tracking abort controllers per
FieldApi instance or by ignoring async completions when the instance no longer
matches. Concretely: change the validation logic so each FieldApi stores its own
controllers (e.g., a per-instance map on this) instead of reusing
fieldInfo.validationMetaMap.lastAbortController, and abort only those on
teardown; or add a guard in the async validator completion path (before calling
field.setMeta(...) / inside FieldApi's async result handling) that checks
field.getInfo().instance === this and returns early if it differs. Ensure you
reference validationMetaMap, lastAbortController, FieldApi, setMeta and getInfo
when locating the relevant code to update.
- Around line 1330-1359: Currently async validation/listener
timeouts/controllers are attached to the active instance (this) even when the
async work targets a different field, so the target field cannot cancel them on
unmount; update the code that schedules linked-field async work (the
validateAsync caller that writes to getInfo().validationMetaMap and
this.timeoutIds.validations / this.timeoutIds.listeners) to store ownership on
the target field instead of this (i.e., use the target field's timeoutIds and
validationMetaMap entry keys), and update the teardown returned by the FieldApi
cleanup to clear any timeouts/controllers stored on the field itself (iterate
and clear entries on field.timeoutIds.validations/listeners/formListeners and
remove related validationMetaMap entries) so the target field’s unmount cancels
its own async work. Ensure you reference and update validateAsync,
getInfo().validationMetaMap, and timeoutIds.validations/listeners/formListeners
usage sites so ownership is moved to the field.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 39798198-5e48-4350-8160-450fd6a0e1ea

📥 Commits

Reviewing files that changed from the base of the PR and between 10abde0 and 8b41487.

📒 Files selected for processing (3)
  • packages/form-core/src/FieldApi.ts
  • packages/form-core/tests/FieldApi.spec.ts
  • packages/react-form/tests/useField.test.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant